/**
 * @author Vitor Franco do Carmo (vitor.carmo@cpmbraxis.com)
 * @since 28/07/2013
 *
 */
package br.com.vfcfw.persistence.dao;

import java.io.Serializable;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;

import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;

import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.NonUniqueResultException;
import org.hibernate.ObjectDeletedException;
import org.hibernate.PersistentObjectException;
import org.hibernate.PropertyValueException;
import org.hibernate.QueryException;
import org.hibernate.Session;
import org.hibernate.SessionException;
import org.hibernate.StaleObjectStateException;
import org.hibernate.StaleStateException;
import org.hibernate.TransactionException;
import org.hibernate.TransientObjectException;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.WrongClassException;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.query.AuditQueryCreator;
import org.hibernate.envers.query.criteria.AuditCriterion;
import org.hibernate.envers.query.criteria.SimpleAuditExpression;
import org.hibernate.envers.query.property.PropertyNameGetter;
import org.hibernate.envers.query.property.RevisionNumberPropertyName;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.exception.JDBCConnectionException;
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.transform.AliasToBeanResultTransformer;

import br.com.vfcfw.commons.domain.entity.BaseEntity;
import br.com.vfcfw.commons.domain.filter.BaseFilter;
import br.com.vfcfw.commons.util.ConstantesArquitetura;
import br.com.vfcfw.commons.util.GenericsUtil;
import br.com.vfcfw.commons.util.LogUtil;
import br.com.vfcfw.commons.util.MensagemUtil;
import br.com.vfcfw.commons.util.SQLText;
import br.com.vfcfw.commons.util.StringUtil;
import br.com.vfcfw.persistence.exception.ConstraintException;

/**
 * Classe base para todos os DAOs que utilizam JPA e Hibernate.
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@cpmbraxis.com)
 * @since 28/07/2013
 * 
 */
public abstract class BaseJPAHibernateDAO<E extends BaseEntity> {

    private static final Logger LOGGER = Logger.getLogger(BaseJPAHibernateDAO.class);

    protected static final String SELECT = "SELECT ";

    protected static final String FROM = "FROM ";

    protected static final String DELETE = "DELETE ";

    protected static final String WHERE = "WHERE ";

    protected static final String PERCENT = "%";

    private static final int CodErroExclusaoViolada = 2292;

    private EntityManager entityManager;

    @SuppressWarnings("unchecked")
    private Class<E> entityClass = GenericsUtil.getSuperClassGenericType(this.getClass());

    /**
     * Construtor da classe.
     */
    public BaseJPAHibernateDAO() {

        super();
    }

    /**
     * Construtor da classe.
     * 
     * @param entityManager {@link EntityManager}.
     */
    public BaseJPAHibernateDAO(EntityManager entityManager) {

        super();
        this.entityManager = entityManager;
    }

    /**
     * Obtém a entidade persistente pela chave primária.
     * 
     * @param entity Entidade persistente.
     * @return Entidade persistente.
     */
    public E obterRegistroPorId(E entity) {

        return obterRegistroPorId(entity.getChavePrimaria());
    }

    /**
     * Obtém a entidade persistente pela chave primária.
     * 
     * @param id Identificador da entidade persistente (chave primária).
     * @return Entidade persistente.
     */
    public E obterRegistroPorId(Serializable id) {

        E registro = null;

        LogUtil.debug(LOGGER, String.format("obterRegistroPorId: %s (%s).", getEntityClass().getSimpleName(), id));

        try {

            registro = getEntityManager().find(getEntityClass(), id);

        } catch (NoResultException e) {
            LogUtil.debug(LOGGER,
                String.format("Objeto %s com id %s não encontrado.", getEntityClass().getSimpleName(), id));
        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registro;
    }

    /**
     * Retorna um registro baseado no critério de pesquisa.
     * 
     * @param detachedCriteria {@link DetachedCriteria} Critério de pesquisa.
     * @return Entidade persistente.
     */
    @SuppressWarnings("unchecked")
    public E obterRegistroPorCriterio(DetachedCriteria detachedCriteria) {

        E registro = null;

        LogUtil.debug(LOGGER, String.format("obterRegistroPorCriterio: %s.", getEntityClass().getSimpleName()));

        try {

            Criteria criteria = detachedCriteria.getExecutableCriteria(getSessaoHibernate());
            criteria.setMaxResults(1);

            registro = (E) criteria.uniqueResult();

        } catch (NoResultException e) {
            LogUtil.debug(LOGGER, String.format("Objeto %s não encontrado.", getEntityClass().getSimpleName()));
        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registro;
    }

    /**
     * Retorna um registro baseado no filtro.
     * 
     * @param sql {@link SQLText} Filtro de pesquisa.
     * @return Entidade persistente.
     */
    @SuppressWarnings("unchecked")
    public E obterRegistroPorFiltro(SQLText sql) {

        E registro = null;

        LogUtil.debug(LOGGER, String.format("obterRegistroPorFiltro: %s.", getEntityClass().getSimpleName()));

        try {

            Query query = getEntityManager().createQuery(sql.obterSentencaSQL());

            copiarParametros(sql, query);
            query.setMaxResults(1);

            registro = (E) query.getSingleResult();

        } catch (NoResultException e) {
            LogUtil.debug(LOGGER, String.format("Objeto %s não encontrado.", getEntityClass().getSimpleName()));
        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registro;
    }

    /**
     * Retorna uma lista de registros baseada nos critérios de pesquisa.
     * 
     * @param filter {@link BaseFilter} Critérios de pesquisa.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @return Lista da entidade persistente.
     */
    public List<E> obterRegistrosPorCriterio(BaseFilter<E> filter, Integer registroInicial, Integer qtdRegistros) {

        return obterRegistrosPorCriterio(filter.getDetachedCriteria(), registroInicial, qtdRegistros);
    }

    /**
     * Retorna uma lista de registros baseada nos critérios de pesquisa.
     * 
     * @param detachedCriteria {@link DetachedCriteria} Critérios de pesquisa.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @return Lista da entidade persistente.
     */
    @SuppressWarnings("unchecked")
    public List<E> obterRegistrosPorCriterio(DetachedCriteria detachedCriteria, Integer registroInicial,
        Integer qtdRegistros) {

        List<E> registros = null;

        LogUtil.debug(LOGGER, String.format("obterRegistrosPorCriterio: %s.", getEntityClass().getSimpleName()));

        try {

            Criteria criteria = detachedCriteria.getExecutableCriteria(getSessaoHibernate());

            if (possuiValor(registroInicial)) {

                criteria.setFirstResult(registroInicial.intValue());
            }

            if (possuiValor(qtdRegistros)) {

                criteria.setMaxResults(qtdRegistros);
            }

            registros = criteria.list();

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registros;
    }

    /**
     * Retorna uma lista de registros do tipo business baseado nos critérios de pesquisa.
     * 
     * @param detachedCriteria {@link DetachedCriteria} Critérios de pesquisa.
     * @param classeBusiness Classe do tipo business.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @return Lista do objeto business.
     */
    @SuppressWarnings("unchecked")
    public <B> List<B> obterRegistrosBusinessPorCriterio(DetachedCriteria detachedCriteria, Class<B> classeBusiness,
        Integer registroInicial, Integer qtdRegistros) {

        List<B> registros = null;

        LogUtil
            .debug(LOGGER, String.format("obterRegistrosBusinessPorCriterio: %s.", getEntityClass().getSimpleName()));

        try {

            Criteria criteria = detachedCriteria.getExecutableCriteria(getSessaoHibernate());

            if (possuiValor(registroInicial)) {

                criteria.setFirstResult(registroInicial.intValue());
            }

            if (possuiValor(qtdRegistros)) {

                criteria.setMaxResults(qtdRegistros);
            }

            criteria.setResultTransformer(new AliasToBeanResultTransformer(classeBusiness));

            registros = criteria.list();

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registros;
    }

    /**
     * Retorna uma lista de registros baseada no filtro de pesquisa.
     * 
     * @param filter {@link BaseFilter} Filtro de pesquisa.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @param ordenacoes Array com os critérios de ordenação.
     * @return Lista da entidade persistente.
     */
    public List<E> obterRegistrosPorFiltro(BaseFilter<E> filter, Integer registroInicial, Integer qtdRegistros,
        Order... ordenacoes) {

        return obterRegistrosPorFiltro(filter.getSQL(), registroInicial, qtdRegistros, ordenacoes);
    }

    /**
     * Retorna uma lista de registros baseada no filtro de pesquisa.
     * 
     * @param sql {@link SQLText} Filtro de pesquisa.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @param ordenacoes Array com os critérios de ordenação.
     * @return Lista da entidade persistente.
     */
    @SuppressWarnings("unchecked")
    public List<E> obterRegistrosPorFiltro(SQLText sql, Integer registroInicial, Integer qtdRegistros,
        Order... ordenacoes) {

        List<E> registros = null;

        LogUtil.debug(LOGGER, String.format("obterRegistrosPorFiltro: %s.", getEntityClass().getSimpleName()));

        try {

            if (ordenacoes != null) {

                sql.adicionar(criaInstrucaoOrdenacao(ordenacoes));
            }

            Query query = getEntityManager().createQuery(sql.obterSentencaSQL());

            copiarParametros(sql, query);

            if (possuiValor(registroInicial)) {

                query.setFirstResult(registroInicial.intValue());
            }

            if (possuiValor(qtdRegistros)) {

                query.setMaxResults(qtdRegistros);
            }

            registros = query.getResultList();

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registros;
    }

    /**
     * Retorna uma lista de registros do tipo business baseado no filtro de pesquisa.
     * 
     * @param sql {@link SQLText} Filtro de pesquisa.
     * @param classeBusiness Classe do tipo business.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @param ordenacoes Array com os critérios de ordenação.
     * @return Lista da entidade persistente.
     */
    @SuppressWarnings("unchecked")
    public <B> List<B> obterRegistrosBusinessPorFiltro(SQLText sql, Class<B> classeBusiness, Integer registroInicial,
        Integer qtdRegistros, Order... ordenacoes) {

        List<B> registros = null;

        LogUtil.debug(LOGGER, String.format("obterRegistrosBusinessPorFiltro: %s.", getEntityClass().getSimpleName()));

        try {

            if (ordenacoes != null) {

                sql.adicionar(criaInstrucaoOrdenacao(ordenacoes));
            }

            Query query = getEntityManager().createQuery(sql.obterSentencaSQL());

            copiarParametros(sql, query);

            if (possuiValor(registroInicial)) {

                query.setFirstResult(registroInicial.intValue());
            }

            if (possuiValor(qtdRegistros)) {

                query.setMaxResults(qtdRegistros);
            }

            registros = query.getResultList();

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registros;
    }

    /**
     * Retorna uma lista de registros baseada no exemplo de pesquisa.
     * 
     * @param entity {@link BaseEntity} Exemplo de pesquisa.
     * @param registroInicial Registro inicial.
     * @param qtdRegistros Quantidade de registros máximos a ser retornados.
     * @param ordenacoes Array com os critérios de ordenação.
     * @return Lista da entidade persistente.
     */
    @SuppressWarnings("unchecked")
    public List<E> obterRegistrosPorExemplo(E entity, Integer registroInicial, Integer qtdRegistros,
        Order... ordenacoes) {

        List<E> registros = null;

        LogUtil.debug(LOGGER, String.format("obterRegistrosPorExemplo: %s.", getEntityClass().getSimpleName()));

        try {

            Example exemplo = criarExemplo(entity, true, MatchMode.START, true, true);

            Criteria criteria = getSessaoHibernate().createCriteria(getEntityClass());
            criteria.add(exemplo);

            if (possuiValor(registroInicial)) {

                criteria.setFirstResult(registroInicial.intValue());
            }

            if (possuiValor(qtdRegistros)) {

                criteria.setMaxResults(qtdRegistros);
            }

            for (Order ordenacao : ordenacoes) {

                criteria.addOrder(ordenacao);
            }

            registros = criteria.list();

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return registros;
    }

    /**
     * Retorna a quantidade de registros baseado no filtro de pesquisa.
     * 
     * @param filter {@link BaseFilter} Filtro de pesquisa.
     * @param filtroPorCriteria <tt>TRUE</tt> usa {@link Criteria} critérios de pesquisa, <tt>FALSE</tt> usa
     *        {@link Query} JPQL/HQL.
     * @return Quantidade de registros.
     */
    public int obterQuantidadeRegistros(BaseFilter<E> filter, boolean filtroPorCriteria) {

        int qtdRegistros = 0;

        if (filtroPorCriteria) {

            qtdRegistros = obterQuantidadeRegistrosPorCriterio(filter.getDetachedCriteria());
        } else {
            qtdRegistros = obterQuantidadeRegistrosPorFiltro(filter.getSQL());
        }
        return qtdRegistros;
    }

    /**
     * Retorna a quantidade de registros baseado nos critérios de pesquisa.
     * 
     * @param detachedCriteria {@link DetachedCriteria} Critérios de pesquisa.
     * @return Quantidade de registros.
     */
    public int obterQuantidadeRegistrosPorCriterio(DetachedCriteria detachedCriteria) {

        Long qtdRegistros = 0L;

        LogUtil.debug(LOGGER,
            String.format("obterQuantidadeRegistrosPorCriterio: %s.", getEntityClass().getSimpleName()));

        try {

            Criteria criteria = detachedCriteria.getExecutableCriteria(getSessaoHibernate());

            qtdRegistros = (Long) criteria.setProjection((Projections.rowCount()))
                .setResultTransformer(Criteria.PROJECTION).uniqueResult();

            if (!possuiValor(qtdRegistros)) {

                qtdRegistros = 0L;
            }

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return qtdRegistros.intValue();
    }

    /**
     * Retorna a quantidade de registros baseado no filtro de pesquisa.
     * 
     * @param sql {@link SQLText} Filtro de pesquisa.
     * @return Quantidade de registros.
     */
    public int obterQuantidadeRegistrosPorFiltro(SQLText sql) {

        Long qtdRegistros = 0L;

        LogUtil
            .debug(LOGGER, String.format("obterQuantidadeRegistrosPorFiltro: %s.", getEntityClass().getSimpleName()));

        try {

            Query query = getEntityManager().createQuery(sql.obterSentencaSQL());

            copiarParametros(sql, query);

            if (!possuiValor(qtdRegistros)) {

                qtdRegistros = 0L;
            }

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return qtdRegistros.intValue();
    }

    /**
     * Retorna a quantidade de registros baseado no exemplo de pesquisa.
     * 
     * @param entity {@link BaseEntity} Exemplo de pesquisa.
     * @param usarLike <tt>TRUE</tt> para usar LIKE nos campos, <tt>FALSE</tt> para usar igualdade nos campos.
     * @param modo {@link MatchMode} Modo de pesquisa usando LIKE.
     * @param excluirZeros <tt>TRUE</tt> exclui os campos númerico com valor zero na pesquisa, <tt>FALSE</tt> inclui os
     *        campos númerico com valor zero na pesquisa.
     * @param ignoreCase <tt>TRUE</tt> pesquisa ignorando o case, <tt>FALSE</tt> pesquisa usando case sensitive.
     * 
     * @return Quantidade de registros.
     */
    public int obterQuantidadeRegistrosPorExemplo(E entity, boolean usarLike, MatchMode modo, boolean excluirZeros,
        boolean ignoreCase) {

        Long qtdRegistros = 0L;

        LogUtil.debug(LOGGER,
            String.format("obterQuantidadeRegistrosPorExemplo: %s.", getEntityClass().getSimpleName()));

        try {

            Example exemplo = criarExemplo(entity, usarLike, modo, excluirZeros, ignoreCase);

            Criteria criteria = getSessaoHibernate().createCriteria(getEntityClass());
            criteria.add(exemplo);

            qtdRegistros = (Long) criteria.setProjection((Projections.rowCount()))
                .setResultTransformer(Criteria.PROJECTION).uniqueResult();

            if (!possuiValor(qtdRegistros)) {

                qtdRegistros = 0L;
            }

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return qtdRegistros.intValue();
    }

    /**
     * Inclui um registro da entidade persistente.
     * 
     * @param entity Entidade persistente.
     */
    public E incluir(E entity) {

        E entityIncluido = null;

        LogUtil.debug(LOGGER,
            String.format("incluir: %s (%s).", getEntityClass().getSimpleName(), entity.getChavePrimaria()));

        try {

            getEntityManager().persist(entity);
            getEntityManager().flush();
            getEntityManager().refresh(entity);
            entityIncluido = entity;

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return entityIncluido;
    }

    /**
     * Persiste uma lista da entidade persistente.
     * 
     * @param listEntity {@link List} Lista da entidade persistente.
     * @return {@link List} Lista com as entidades persistidas.
     */
    public List<E> incluir(List<E> listEntity) throws PersistenceException {

        List<E> listResult = new ArrayList<E>();
        E entityIncluido = null;

        try {

            for (int i = 0; i < listEntity.size(); i++) {

                entityIncluido = listEntity.get(i);
                getEntityManager().persist(entityIncluido);

                listResult.add(entityIncluido);

                if (i % 20 == 0) {

                    getEntityManager().flush();
                    getEntityManager().clear();
                }
            }
        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return listResult;
    }

    /**
     * Altera o registro da entidade persistente.
     * 
     * @param entity Entidade persistente.
     */
    public E alterar(E entity) {

        E entityAlterado = null;

        LogUtil.debug(LOGGER,
            String.format("alterar: %s (%s).", getEntityClass().getSimpleName(), entity.getChavePrimaria()));

        try {

            entityAlterado = getEntityManager().merge(entity);

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return entityAlterado;
    }

    /**
     * Exclui o registro da entidade persistente.
     * 
     * @param entity Entidade persistente.
     */
    public void excluir(E entity) {

        try {

            excluir(entity.getChavePrimaria());

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
    }

    /**
     * Exclui o registro da entidade persistente.
     * 
     * @param id Identificador da entidade persistente (chave primária).
     */
    public void excluir(Serializable id) {

        LogUtil.debug(LOGGER, String.format("excluir: %s (%s).", getEntityClass().getSimpleName(), id));

        try {

            E entityExclusao = obterRegistroPorId(id);
            getEntityManager().remove(entityExclusao);

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
    }

    /**
     * Exclui uma lista de registros de uma entidade.
     * 
     * @param listEntity {@link List} de registros da entidade mapeada com JPA.
     */
    public void excluir(List<E> listEntity) {

        E entityExclusao = null;

        try {

            for (int i = 0; i < listEntity.size(); i++) {

                entityExclusao = obterRegistroPorId(listEntity.get(i).getChavePrimaria());
                getEntityManager().remove(entityExclusao);

                if (i % 20 == 0) {

                    getEntityManager().flush();
                    getEntityManager().clear();
                }
            }
        } catch (Exception e) {
            tratarException(e);
        }
    }

    /**
     * Atualiza a entidade persistente sincronizando-o com a base.
     * 
     * @param entity Entidade persistente.
     * @return Entidade persistente.
     * @see javax.persistence.EntityManager#refresh(Object)
     */
    public E atualizar(E entity) {

        E entityAtualizado = null;

        LogUtil.debug(LOGGER,
            String.format("atualizar: %s (%s).", getEntityClass().getSimpleName(), entity.getChavePrimaria()));

        try {

            getEntityManager().refresh(entity);
            entityAtualizado = entity;

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return entityAtualizado;
    }

    /**
     * Atualiza a base de dados.
     * 
     * @see javax.persistence.EntityManager#flush()
     */
    public void atualizarBanco() {

        LogUtil.debug(LOGGER, "Atualizando base de dados.");

        getEntityManager().flush();
    }

    /**
     * Remove o gerenciamento da sessão do JPA/Hibernate da entidade persistente.
     * 
     * @param entity Entidade persistente.
     * @see org.hibernate.Session#evict(Object)
     */
    public void removerGerenciamentoSessao(E entity) {

        LogUtil.debug(
            LOGGER,
            String.format("removerGerenciamentoSessao: %s (%s).", getEntityClass().getSimpleName(),
                entity.getChavePrimaria()));

        try {

            getSessaoHibernate().evict(entity);

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
    }

    /**
     * Limpa o contexto de persistência, removendo o gerenciamento da sessão do JPA/Hibernate de todas as entidades
     * persistentes.
     * 
     * @see javax.persistence.EntityManager#clear()
     */
    public void limparSessao() {

        LogUtil.debug(LOGGER, "Limpando a sessão do JPA/Hibernate.");

        getEntityManager().clear();
    }

    /**
     * Retorna a conexão com o banco de dados.
     * 
     * @return {@link Connection}.
     */
    public Connection getConexao() {

        Connection conexao = null;

        LogUtil.debug(LOGGER, "Obtendo a conexão com o banco de dados.");

        try {

            conexao = getEntityManager().unwrap(Connection.class);

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return conexao;
    }

    /**
     * Retorna o valor do atributo entityManager.
     * 
     * @return Valor do atributo entityManager.
     */
    public EntityManager getEntityManager() {

        LogUtil.debug(LOGGER, "Obtendo o EntityManager.");

        return entityManager;
    }

    /**
     * Especifica o valor do atributo entityManager.
     * 
     * @param entityManager EntityManager do entityManager a ser setado.
     */
    public void setEntityManager(EntityManager entityManager) {

        LogUtil.debug(LOGGER, "Especificando o EntityManager.");

        this.entityManager = entityManager;
    }

    /**
     * Retorna o valor do atributo classeEntity.
     * 
     * @return Valor do atributo classeEntity.
     */
    public Class<E> getEntityClass() {

        return entityClass;
    }

    /**
     * Especifica o valor do atributo entityClass.
     * 
     * @param entityClass Class<E> do entityClass a ser setado.
     */
    public void setEntityClass(Class<E> entityClass) {

        this.entityClass = entityClass;
    }

    /**
     * Retorna a sessão do hibernate.
     * 
     * @return Sessão do hibernate.
     */
    protected Session getSessaoHibernate() {

        Session session = null;

        LogUtil.debug(LOGGER, "Obtendo a sessão do Hibernate.");

        try {

            session = (Session) getEntityManager().getDelegate();

            if (!session.isOpen()) {
                session = session.getSessionFactory().openSession();
            }
        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return session;
    }

    /**
     * Retorna a instrução de ordenação (<tt>ORDER BY</tt>), para ser adicionada em uma pesquisa JPQL/HQL.
     * 
     * @param ordenacoes Array com os critérios de ordenação.
     * @return Instrução
     */
    protected String criaInstrucaoOrdenacao(Order... ordenacoes) {

        StringBuilder strOrdenacoes = new StringBuilder();

        if (ordenacoes.length > 0) {

            strOrdenacoes.append(" ORDER BY ");

            for (int indice = 0; indice < ordenacoes.length; indice++) {

                strOrdenacoes.append(ordenacoes[indice].toString());

                if (indice < ordenacoes.length - 1) {

                    strOrdenacoes.append(", ");
                }
            }
        }
        return strOrdenacoes.toString();
    }

    /**
     * Define o modo de atualização da base de dados.
     * 
     * @param modoAtualizacao {@link FlushModeType} Modo de atualização.
     */
    protected void definirModoAtualizacao(FlushModeType modoAtualizacao) {

        LogUtil.debug(LOGGER, "Definindo o modo de atualização da base de dados.");

        getEntityManager().setFlushMode(modoAtualizacao);
    }

    /**
     * Verifica se o valor é nulo ou vazio.
     * 
     * @param valor Valor.
     * @return <tt>TRUE</tt> se não for <b>nulo</b> ou <b>vazio</b>, <tt>FALSE</tt> caso contrário.
     */
    protected boolean possuiValor(Object valor) {

        return StringUtil.possuiValor(valor);
    }

    /**
     * Realiza o tratamento das exceptions
     * 
     * @param e {@link Exception}.
     * @throws PersistenceException
     */
    protected void tratarException(Exception e) throws PersistenceException {

        if (e instanceof TransactionException) {

            TransactionException transactionException = null;

            if (e.getCause() != null) {

                if (e.getCause().getCause() != null) {

                    transactionException = (TransactionException) e.getCause().getCause();
                } else {

                    transactionException = (TransactionException) e.getCause();
                }
            } else {

                transactionException = (TransactionException) e;
            }

            tratarTransactionException(transactionException);

        } else if (e instanceof SessionException) {

            throw new PersistenceException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_CONEXAO_BANCO), e);
        } else if (e.getCause() != null && e.getCause() instanceof ConstraintViolationException) {
            if (((ConstraintViolationException) e.getCause()).getErrorCode() == CodErroExclusaoViolada) {
                throw new ConstraintException(
                    MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_EXCLUSAO_BLOQUEADA), e);
            }
        }
        throw new PersistenceException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_BANCO_DADOS), e);
    }

    /**
     * Verifica se entidade persistente é auditável.
     * 
     * @return <tt>TRUE</tt> se for auditável, <tt>FALSE</tt> caso contrário.
     */
    protected boolean isClasseEntityAuditavel() {

        boolean auditavel = false;

        LogUtil.debug(LOGGER, "Verificando se a entidade persistente é auditável.");

        try {

            auditavel = getAuditReader().isEntityClassAudited(getEntityClass());

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return auditavel;
    }

    /**
     * Retorna uma entidade persistente pelo seu identificador e número de revisão.
     * 
     * @param id Identificador da entidade persistente.
     * @param revisao Número da revisão.
     * @return Entidade persistente.
     */
    protected E obterRevisaoPorId(Serializable id, Number revisao) {

        E entityRevisao = null;

        LogUtil.debug(LOGGER,
            String.format("obterRevisaoPorId: %s (%s), revisão: %s.", getEntityClass().getSimpleName(), id, revisao));

        try {

            entityRevisao = getAuditReader().find(getEntityClass(), id, revisao);

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return entityRevisao;
    }

    /**
     * Retorna uma lista de revisões da entidade persistente baseada no número da revisão.
     * 
     * @param revisao Número da revisão.
     * @return Lista de revisões.
     */
    @SuppressWarnings("unchecked")
    protected List<E> obterRevisoes(Number revisao) {

        List<E> revisoes = null;

        LogUtil.debug(LOGGER,
            String.format("obterRevisoes: %s revisão: %s.", getEntityClass().getSimpleName(), revisao));

        try {

            AuditQueryCreator query = getAuditReader().createQuery();

            PropertyNameGetter getter = new RevisionNumberPropertyName();
            AuditCriterion criterion = new SimpleAuditExpression(getter, revisao, "=");

            revisoes = query.forEntitiesAtRevision(getEntityClass(), revisao).add(criterion).getResultList();

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return revisoes;
    }

    /**
     * Retorna o {@link AuditReader}.
     * 
     * @return {@link AuditReader}.
     */
    private AuditReader getAuditReader() {

        AuditReader reader = null;

        LogUtil.debug(LOGGER, "Obtendo o reader de auditoria.");

        try {

            reader = AuditReaderFactory.get(entityManager);

        } catch (Exception e) {
            throw new PersistenceException(e);
        }
        return reader;
    }

    /**
     * Copia os parâmetros definidos no {@link SQLText} na {@link Query} do JPA.
     * 
     * @param sql {@link SQLText}.
     * @param query {@link Query}.
     */
    private void copiarParametros(SQLText sql, Query query) {

        LogUtil.debug(LOGGER, "Copiando parâmetros para a Query.");

        for (Entry<String, Object> entry : sql.obterParametrosNomeados().entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Cria o exemplo que será usado na pesquisa.
     * 
     * @param entity Entidade persistente
     * @param usarLike <tt>TRUE</tt> para usar LIKE nos campos, <tt>FALSE</tt> para usar igualdade nos campos.
     * @param modo {@link MatchMode} Modo de pesquisa usando LIKE.
     * @param excluirZeros <tt>TRUE</tt> exclui os campos númerico com valor zero na pesquisa, <tt>FALSE</tt> inclui os
     *        campos númerico com valor zero na pesquisa.
     * @param ignoreCase <tt>TRUE</tt> pesquisa ignorando o case, <tt>FALSE</tt> pesquisa usando case sensitive.
     * @return {@link Example}.
     */
    private Example criarExemplo(E entity, boolean usarLike, MatchMode modo, boolean excluirZeros, boolean ignoreCase) {

        LogUtil.debug(LOGGER, "Criando um Exemplo de pesquisa.");

        Example exemplo = Example.create(entity);

        if (usarLike) {

            if (modo != null) {

                exemplo.enableLike(modo);
            } else {
                exemplo.enableLike();
            }
        }

        if (excluirZeros) {
            exemplo.excludeZeroes();
        }

        if (ignoreCase) {

            exemplo.ignoreCase();
        }
        return exemplo;
    }

    /**
     * Realiza o tratamento da {@link TransactionException}.
     * 
     * @param transactionException {@link TransactionException}.
     */
    private void tratarTransactionException(TransactionException transactionException) {

        if (transactionException instanceof HibernateException) {

            HibernateException hibernateException = (HibernateException) transactionException;

            if (hibernateException instanceof JDBCConnectionException) {

                throw new PersistenceException(
                    MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_CONEXAO_BANCO), hibernateException);

            } else if (hibernateException instanceof ConstraintViolationException) {

                ConstraintViolationException constraintViolationException = (ConstraintViolationException) hibernateException;

                throw new PersistenceException(
                    MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_CONSTRAINT_VIOLATION),
                    constraintViolationException);

            } else if (hibernateException instanceof LockAcquisitionException) {

                LockAcquisitionException lockAcquisitionException = (LockAcquisitionException) hibernateException;

                throw new PersistenceException(
                    MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OPERACAO_BLOQUEADA),
                    lockAcquisitionException);

            } else if (hibernateException instanceof JDBCException) {

                JDBCException jdbcException = null;

                if (hibernateException.getCause() != null) {

                    jdbcException = (JDBCException) hibernateException.getCause();
                } else {
                    jdbcException = (JDBCException) hibernateException;
                }

                throw new PersistenceException(String.format("JDBCException - %s", jdbcException.getMessage()),
                    jdbcException);

            } else if (hibernateException instanceof PropertyValueException) {

                PropertyValueException propertyValueException = (PropertyValueException) hibernateException;

                throw new PersistenceException(String.format("PropertyValueException - %s",
                    propertyValueException.getMessage()), propertyValueException);

            } else if (hibernateException instanceof PersistentObjectException) {

                PersistentObjectException persistentObjectException = (PersistentObjectException) hibernateException;

                throw new PersistenceException(String.format("PersistentObjectException - %s",
                    persistentObjectException.getMessage()), persistentObjectException);

            } else if (hibernateException instanceof TransientObjectException) {

                TransientObjectException transientObjectException = (TransientObjectException) hibernateException;

                throw new PersistenceException(String.format("TransientObjectException - %s",
                    transientObjectException.getMessage()), transientObjectException);

            } else if (hibernateException instanceof ObjectDeletedException) {

                ObjectDeletedException objectDeletedException = (ObjectDeletedException) hibernateException;

                throw new PersistenceException(String.format("ObjectDeletedException - %s",
                    objectDeletedException.getMessage()), objectDeletedException);

            } else if (hibernateException instanceof QueryException) {

                QueryException queryException = (QueryException) hibernateException;

                throw new PersistenceException(String.format("QueryException - %s", queryException.getMessage()),
                    queryException);

            } else if (hibernateException instanceof UnresolvableObjectException) {

                UnresolvableObjectException unresolvableObjectException = (UnresolvableObjectException) hibernateException;

                throw new PersistenceException(String.format("UnresolvableObjectException - %s",
                    unresolvableObjectException.getMessage()), unresolvableObjectException);

            } else if (hibernateException instanceof WrongClassException) {

                WrongClassException wrongClassException = (WrongClassException) hibernateException;

                throw new PersistenceException(String.format("WrongClassException - %s",
                    wrongClassException.getMessage()), wrongClassException);

            } else if (hibernateException instanceof NonUniqueResultException) {

                NonUniqueResultException nonUniqueResultException = (NonUniqueResultException) hibernateException;

                throw new PersistenceException(String.format("NonUniqueResultException - %s",
                    nonUniqueResultException.getMessage()), nonUniqueResultException);

            } else if (hibernateException instanceof StaleObjectStateException) {

                StaleObjectStateException staleObjectStateException = (StaleObjectStateException) hibernateException;

                throw new PersistenceException(String.format("StaleObjectStateException - %s",
                    staleObjectStateException.getMessage()), staleObjectStateException);

            } else if (hibernateException instanceof StaleStateException) {

                StaleStateException staleStateException = (StaleStateException) hibernateException;

                throw new PersistenceException(String.format("StaleStateException - %s",
                    staleStateException.getMessage()), staleStateException);

            } else {

                throw new PersistenceException(
                    MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_CONEXAO_BANCO), transactionException);
            }
        } else {

            throw new PersistenceException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_CONEXAO_BANCO),
                transactionException);
        }
    }

}
